﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="WeakDelegateRuntime.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// <summary>
//   Defines the WeakDelegateRuntime type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon
{
    using System;
    using System.Linq;
    using System.Reflection;
    using System.Reflection.Emit;
    using System.Threading;

    public static class WeakDelegateRuntime
    {
        public static bool EnsureInitialized<TDelegate>(Func<IDelegatable, TDelegate> bindMethod = null)
        {
            ThrowIfTypeIsNotADelegate(typeof(TDelegate));
            return Binder<TDelegate>.EnsureInitialized(bindMethod);
        }

        public static TDelegate Bind<TDelegate>(IDelegatable delegatable)
        {
            ThrowIfTypeIsNotADelegate(typeof(TDelegate));
            return Binder<TDelegate>.Bind(delegatable);
        }

        internal static Delegate CastToDelegate<TDelegate>(TDelegate @delegate)
        {
            ThrowIfTypeIsNotADelegate(typeof(TDelegate));
            return (Delegate)(object)@delegate;
        }

        internal static void ThrowIfTypeIsNotADelegate(Type type)
        {
            if (!(typeof(Delegate).IsAssignableFrom(type)))
            {
                throw new NotSupportedException(ExceptionMessages.WeakDelegateTypeArgumentNotSupported);
            }
        }

        #region Nested type: Binder

        internal class Binder<TDelegate>
        {
            #region Fields

            private static Func<IDelegatable, TDelegate> _bindMethod;
            private static bool? _isTypeSupported;
            private static MethodInfo _invokeMethod;

            #endregion

            public static MethodInfo InvokeMethod
            {
                get { return _invokeMethod ?? (_invokeMethod = typeof(TDelegate).GetMethod("Invoke")); }
            }

            public static TDelegate Bind(IDelegatable delegatable)
            {
                EnsureInitialized();
                return _bindMethod(delegatable);
            }

            public static bool EnsureInitialized(Func<IDelegatable, TDelegate> bindMethod = null)
            {
                if (_bindMethod != null)
                {
                    return false;
                }

                if (_isTypeSupported == null)
                {
                    var onInvokedMethod = BindToInvokeMethod();

                    // delegate type is supported, but may still be inaccessible, e.g. it is non-pubic and requires a custom factory method
                    _isTypeSupported = onInvokedMethod != null;
                    if (_isTypeSupported == true)
                    {
                        // bind to delegate constructor
                        var delegateConstructor = BindToDelegateConstructor();
                        if (delegateConstructor != null)
                        {
                            // create dynamic method
                            bindMethod = EmitDelegateFactoryMethod(onInvokedMethod, delegateConstructor) ?? bindMethod;
                        }
                    }
                }

                if (_isTypeSupported == false)
                {
                    throw new NotSupportedException(ExceptionMessages.WeakDelegateTypeArgumentNotSupported);
                }

                var isAlreadyInitialized = Interlocked.CompareExchange(ref _bindMethod, bindMethod, null) != null;
                if (!isAlreadyInitialized && bindMethod == null)
                {
                    throw new NotSupportedException(ExceptionMessages.WeakDelegateTypeMissingDelegateFactoryMethod);
                }

                return !isAlreadyInitialized;
            }

            private static Func<IDelegatable, TDelegate> EmitDelegateFactoryMethod(MethodInfo invokeMethod,
                ConstructorInfo delegateConstructor)
            {

#if SILVERLIGHT
                var method = new DynamicMethod("Invoke", typeof(TDelegate),
                    new[] { typeof(IDelegatable) });
#else
                var method = new DynamicMethod("Invoke", typeof(TDelegate),
                    new[] { typeof(IDelegatable) }, true);
#endif

                var ilGenerator = method.GetILGenerator();
                ilGenerator.Emit(OpCodes.Ldarg_0);
                if (invokeMethod.IsVirtual)
                {
                    ilGenerator.Emit(OpCodes.Dup);
                    ilGenerator.Emit(OpCodes.Ldvirtftn, invokeMethod);
                }
                else
                {
                    ilGenerator.Emit(OpCodes.Ldftn, invokeMethod);
                }
                ilGenerator.Emit(OpCodes.Newobj, delegateConstructor);
                ilGenerator.Emit(OpCodes.Ret);
                return method.CreateDelegate(typeof(Func<IDelegatable, TDelegate>)) as
                    Func<IDelegatable, TDelegate>;
            }

            private static MethodInfo BindToInvokeMethod()
            {
                // get invoke method for delegate type
                var invokeMethod = typeof(TDelegate).GetMethod("Invoke");

                // get parameter types
                var genericArgumentTypes = invokeMethod.GetParameters()
                    .Select(x => x.ParameterType)
                    .ToList();
                if (!invokeMethod.ReturnType.Equals(typeof(void)))
                {
                    genericArgumentTypes.Add(invokeMethod.ReturnType);
                }

                // bind to method matching the specified parameter types
                return invokeMethod.ReturnType.Equals(typeof(void))
                    ? BindToInvokeAction(genericArgumentTypes.ToArray())
                    : BindToInvokeFunc(genericArgumentTypes.ToArray());
            }

            private static MethodInfo BindToInvokeFunc(Type[] genericArgumentTypes)
            {
                const BindingFlags BindingFlags = BindingFlags.Public | BindingFlags.Instance;

                // generic @delegate
                var matchingGenericMethod = typeof(IDelegatable).GetMethods(BindingFlags)
                    .FirstOrDefault(
                        x => x.Name == "InvokeFunc" && x.IsGenericMethod && (x.GetGenericArguments().Length == genericArgumentTypes.Length));
                return matchingGenericMethod == null ? null : matchingGenericMethod.MakeGenericMethod(genericArgumentTypes);
            }

            private static MethodInfo BindToInvokeAction(Type[] parameterTypes)
            {
                const BindingFlags BindingFlags = BindingFlags.Public | BindingFlags.Instance;

                // simple non-generic @delegate
                if (parameterTypes.Length == 0)
                {
                    return typeof(IDelegatable).GetMethod("InvokeAction", BindingFlags, null, new Type[0], null);
                }

                // generic @delegate
                var matchingGenericMethod = typeof(IDelegatable).GetMethods(BindingFlags)
                    .FirstOrDefault(
                        x =>
                            x.Name == "InvokeAction" && x.IsGenericMethod &&
                                (x.GetGenericArguments().Length == parameterTypes.Length));
                return matchingGenericMethod == null ? null : matchingGenericMethod.MakeGenericMethod(parameterTypes);
            }

            private static ConstructorInfo BindToDelegateConstructor()
            {
                if (!CanBindToDelegateConstructor())
                {
                    return null;
                }

                return typeof(TDelegate).GetConstructor(BindingFlags.Public | BindingFlags.Instance,
                    null, new[] { typeof(object), typeof(IntPtr) }, null);
            }

            private static bool CanBindToDelegateConstructor()
            {
#if SILVERLIGHT

                Type delegateType = typeof (TDelegate);
                if (delegateType.IsNestedPrivate || delegateType.IsNotPublic)
                {
                    return false;
                }

                if (delegateType.IsNestedPublic)
                {
                    Type declaringType;
                    while ((declaringType = delegateType.DeclaringType) != null)
                    {
                        if (declaringType.IsNestedPrivate || declaringType.IsNotPublic)
                        {
                            return false;
                        }
                    }
                }

#endif
                return true;
            }
        }

        #endregion
    }
}